package cmich.cps.svn.hooks;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;

import org.apache.commons.io.FileUtils;
/**
 * Handles the post-commit hook.  This includes running JUnit tests found in the unit tests
 * directory on the students newly submitted code.
 * 
 * @author Jonathan Kissinger <kissi1j@cmich.edu>
 *
 */
public class PostCommit {
	private Settings settings;
	private String testingDir;
	private String repoTestDir;
	private String repoUserDir;
	private List<String> files = new ArrayList<String>();
	private String unitTestString = "unit_tests";
	private String assignment;
	private List<String> unitTestClasses = new ArrayList<String>();
	private final static Logger LOGGER = Logger.getLogger(PostCommit.class.getName());

	public PostCommit(String dir, Settings settings) {
		this.settings = settings;
		File testingDir = new File(this.settings.getRepoDir() + File.separator + "testing" + File.separator + settings.getAuthor());
		this.testingDir = testingDir.getAbsolutePath() + File.separator + "src";
		this.repoTestDir = dir + File.separator + this.unitTestString;
		this.repoUserDir = dir + File.separator + settings.getAuthor();
		this.assignment = dir.split(File.separator)[2];
		LOGGER.finer("Test Dir set to: " + this.testingDir);
		LOGGER.finer("Repo Test Dir set to: " + this.repoTestDir);
		LOGGER.finer("Repo User Dir set to: " + this.repoUserDir);
		LOGGER.finer("Assignment set to: " + this.assignment);
	}

	/**
	 * Checks if there are any java files in the repository in the student's folder for
	 * the assignment.
	 * 
	 * @return True if there are java files in the student directory in the repository, false otherwise.
	 */
	public Boolean doFilesExist() {
		String commandResult = this.settings.execCommand(settings.getSvnLook() + " tree " + settings.getRepoDir() + " " + this.repoUserDir);
		String[] files = commandResult.split("\n");
		int numFiles = this.files.size();
		setFiles(files, this.repoUserDir);
		if(numFiles == this.files.size()) {
			LOGGER.fine("setFiles found no files returning java files not found.");
			return false;
		}
		copyFiles(settings.getAuthor());
		return true;
	}

	/**
	 * Checks if any java unit tests are in the repository for the {@link #assignment}
	 * 
	 * @return True if unit tests exist, false otherwise.
	 */
	public Boolean doTestsExist() {
		String commandResult = this.settings.execCommand(settings.getSvnLook() + " tree " + settings.getRepoDir() + " " + this.repoTestDir);
		String[] tests = commandResult.split("\n");
		int numFiles = this.files.size();
		setFiles(tests, this.repoTestDir);
		if(numFiles == this.files.size()) {
			LOGGER.fine("setFiles found no files returning tests not found.");
			return false;
		}
		copyFiles(this.unitTestString);
		return true;
	}

	/**
	 * Sets the paths to each unit test in the ArrayList testFiles.
	 * 
	 * @param tests Array of files in the unit test directory in the repository.
	 */
	public void setFiles(String[] files, String curDir) {
		curDir = curDir + "/";
		LOGGER.fine("Setting files for directory: " + curDir + " " + files.length + " possible files.");
		int depth = 0;
		for(int i=1;i<files.length;i++) {
			int iDepth = getDepth(files[i]);
			if(files[i].endsWith(".java")) {
				if(iDepth>depth) {
					this.files.add(curDir + files[i].trim());
					LOGGER.info("Adding " + curDir + files[i].trim() + " to files");
				}
				else {
					String path = changeDir(depth, iDepth, curDir, "");
					this.files.add(path + files[i].trim());
					LOGGER.info("Adding " + path + files[i].trim() + " to files");
				}
			}
			else if(files[i].endsWith("/")) {
				if(iDepth>depth) {
					depth++;
					String newDir = curDir + files[i].trim();
					curDir = newDir;
				}
				else if(iDepth<=depth) {
					curDir = changeDir(depth, iDepth, curDir, files[i].trim());
					depth = iDepth;
				}
			}
		}
		LOGGER.finer("After setting files, there are " + this.files.size() + " in files List");
	}

	/**
	 * Used by setTests to traverse directories when parsing the "svnlook tree" results.
	 * 
	 * @param depth Current tree depth.
	 * @param iDepth Depth of the current file/directory.
	 * @param curDir Current directory.
	 * @param dir New directory.
	 * @return A string representing the path within the repository of the file or directory passed in.
	 */
	public String changeDir(int depth, int iDepth, String curDir, String dir) {
		int depthDiff = (depth - iDepth) + 1;
		String[] paths = curDir.split("/");
		String newDir = "";
		for(int j=0;j<paths.length-depthDiff;j++) {
			newDir = newDir  + paths[j] + "/";						
		}
		newDir = newDir + dir;
		curDir = newDir;
		return newDir;
	}

	/**
	 * Determines the depth of a directory as determined by the number of spaces preceding it.
	 * This is a methodology used by the "svnlook tree" command.
	 * 
	 * @param string The directory in string format.
	 * @return an integer of the directory's depth in the tree.
	 */
	public int getDepth(String string) {
		int spaces = 0;
		for(int i=0;i<string.length();i++) {
			if(string.charAt(i) == ' ') {
				spaces++;
			}
			else {
				break;
			}
		}
		return spaces;
	}

	/**
	 * Copies the files in the passed directory within the repository out of the
	 * repository using the {@link PostCommit#copyFileFromRepo(String, String)} command.
	 * 
	 * @param fromDir The directory to copy the files from.
	 * @param files A list containing the absolute repository paths to the files.
	 */
	public void copyFiles(String fromDir) {
		LOGGER.fine("Copying files in dir: " + fromDir);
		for(int i=0;i<this.files.size();i++) {
			String fileName = this.files.get(i);
			if(!copyFileFromRepo(fileName)) {
				this.files.remove(this.files.get(i));
			}
		}
	}

	/**
	 * Copies a file out of the repository using the "svnlook cat" command to get
	 * the file contents and then writing it to a new file of the same name
	 * in the testing directory.
	 * 
	 * @param fileName Includes the repository path of the file
	 * @param repoPath The path to the file within the repository
	 */
	public boolean copyFileFromRepo(String fileName) {
		LOGGER.fine("File being copied: " + fileName);
		String contents = settings.execCommand(settings.getSvnLook() + " cat " + settings.getRepoDir() + " " + fileName);
		boolean validTest = fileName.contains(this.unitTestString) && contents.contains("import org.junit") && fileName.endsWith(".java");
		boolean validProgram = !fileName.contains(this.unitTestString) && !contents.contains("import org.junit") && fileName.endsWith(".java");
		if(validTest) {
			addClassName(fileName, contents);
		}
		if(validTest || validProgram) {
			//TODO
			//String sourceDir = File.separator + "src" + File.separator;
			//File newFile = new File(switchDir(fileName).replace(sourceDir, File.separator));
			File newFile = new File(switchDir(fileName));
			File parentDir = newFile.getParentFile();
			if(!parentDir.exists()) {
				parentDir.mkdirs();
			}
			try {
				FileWriter fw = new FileWriter(newFile);
				fw.write(contents);
				fw.close();
			} catch (IOException e) {
				LOGGER.warning(e.toString());
				return false;
			}
			return true;
		}
		else {
			LOGGER.fine("Not copying " + fileName + " because it is not a valid test or valid program.");
			return false;
		}
	}

	/**
	 * Finds the package and class name of the file and adds it to {@link #unitTestClasses}
	 * 
	 * @param fileName Includes the repository path of the file
	 * @param contents Contents of the file
	 */
	public void addClassName(String fileName, String contents) {
		String myPackage = "";
		if(contents.contains("package ")) {
			myPackage = contents.split("package ")[1].split(";")[0];
		}
		String className = myPackage + "." + fileName.split("/")[fileName.split("/").length-1].replace(".java", "");
		LOGGER.fine("Found new classname: " + className);
		this.unitTestClasses.add(className);
	}

	/**
	 * Takes a repository path and replaces it with a disk path
	 * 
	 * @param fileName Includes the repository path of the file
	 * @return The disk path of <code>fileName</code>
	 */
	public String switchDir(String fileName) {
		fileName = fileName.replaceFirst(this.repoTestDir, this.testingDir);
		fileName = fileName.replaceFirst(this.repoUserDir, this.testingDir);
		return fileName;
	}

	/**
	 * Calls {@link #compile(String)} on each File in {@link #files}
	 */
	public void compileFiles() {
		for(String fileName : this.files) {
			//TODO
			//String sourceDir = File.separator + "src" + File.separator;
			//fileName = switchDir(fileName).replace(sourceDir, File.separator);
			fileName = switchDir(fileName);
			LOGGER.info("Compiling: " + fileName);
			compile(fileName);
		}
	}

	/**
	 * Calls javac to compile the given file.  If javac returns anything it's
	 * considered an error and passed to {@link #handleCompileErrors(String)} for handling
	 * 
	 * @param fileName The file to be compiled
	 */
	public void compile(String fileName) {
		String jClassPath = this.testingDir + File.pathSeparator + settings.getJarFile();
		String result = settings.execCommand(settings.getJavac() + " -cp " + jClassPath + " " + fileName);
		if(!result.isEmpty()) {
			handleCompileErrors(result);
		}
	}

	/**
	 * Handles javac compiler errors to give students a better idea of what went wrong.
	 * 
	 * @param result the compiler error to be handled
	 */
	private void handleCompileErrors(String result) {
		LOGGER.fine(result);
		if(result.contains("import ")) {
			String[] lines = result.split("\n");
			for(int i=0;i<lines.length;i++) {
				if(lines[i].contains("import")) {
					String[] packages = lines[i].split(" ")[1].split("\\.");
					String myPackage = "";
					for(int j=0;j<packages.length-1;j++) {
						myPackage = myPackage + packages[j] + ".";
					}
					myPackage = myPackage.substring(0, myPackage.length()-1);
					String file = packages[packages.length-1];
					file = file.substring(0, file.length()-1);
					System.err.println(file + ".java" + " is missing from the package: " + myPackage);
				}
			}
		}
		else {
			String file = result.split("java:")[0] + "java";
			System.err.println("There are compile errors in the file: " + file);
			System.err.println("Please use your IDE to troubleshoot these errors.");
		}
		System.exit(1);
	}

	/**
	 * Executes org.junit.runner.JUnitCore on the classes in {@link #unitTestClasses}
	 * Then gathers the results and passes them on to {@link #handleResults(String)}
	 */
	public void runTests() {
		LOGGER.fine("Running tests");
		String jClassPath = this.testingDir + File.pathSeparator + settings.getJarFile();
		String testClasses = "";
		for(int i=0;i<unitTestClasses.size();i++) {
			testClasses = testClasses + " " + this.unitTestClasses.get(i);
		}
		String result = settings.execCommand("java -cp " + jClassPath + " org.junit.runner.JUnitCore " + testClasses);
		handleResults(result);
	}

	/**
	 * First calls {@link #parseResults(String)} to make the JUnit results easier to read.
	 * Then adds some headers and passes the complete message to {@link SendEmail#send()}
	 * 
	 * @param result The JUnit results
	 */
	private void handleResults(String result) {
		result = parseResults(result);
		String student = "Student: " + settings.getAuthor() + "\n";
		String course = "Course: " + settings.getRepoName() + "\n";
		String assign = "Assignment: " + this.assignment + "\n";
		String dueDate = "Due date: ";
		SimpleDateFormat pretty = new SimpleDateFormat("EEEE MMMM, d 'at' hh:mm:ss aaaa");
		if(settings.getDueDate()==null) {
			dueDate = dueDate + "Unknown \n\n";
		}
		else {
			dueDate = dueDate + pretty.format(settings.getDueDate());
			if(settings.isLate()) {
				dueDate = dueDate + " (Submission is PAST DUE!)\n\n";
			}
			else {
				dueDate = dueDate + " (Submission is on time.)\n\n";
			}
		}
		result =  student + course + assign + dueDate + "Feedback:\n" + result;
		result = result.replaceAll("\n", "\r\n");
		LOGGER.fine("Result:\n" + result);
		String subject = "Feedback for " + this.assignment;
		String note = "\n\nNote: These results are for feedback purposes only and do not represent an actual grade.";
		SendEmail email = new SendEmail(settings.getAuthor(), subject, result + note);
		email.send();
	}

	/**
	 * Parses the JUnit results to make them easier to read.
	 * 
	 * @param result The JUnit result
	 * @return The parsed results
	 */
	public String parseResults(String result) {
		if(result.contains("No test classes found")) {
			LOGGER.config("No test classes found, exiting.");
			System.exit(0);
		}
		if(result.startsWith("Command timed out.")) {
			System.out.println("results: " + result);
			result = "Tests took longer than " + Integer.parseInt(result.split("Timeout=")[1]) + " seconds and timed out.\n";
			return result + "Check your code for infinite loops.\n"; 
		}
		LOGGER.fine("Parsing results.");
		String processedResults = "";
		String testsRun = "";
		String failures = "";
		if(result.contains("FAILURES!!!")) {
			testsRun = result.split("Tests run: ")[1].split(",  Failures: ")[0];
			failures = result.split(",  Failures: ")[1].split("\n")[0];
		}
		if(result.contains("OK (")) {
			testsRun = result.split("OK \\(")[1].split(" tests\\)")[0];
			failures = "0";
		}
		int numTests = Integer.parseInt(testsRun);
		int numFailed = Integer.parseInt(failures);
		int numPassed = numTests - numFailed;
		if(numFailed>0) {
			processedResults = "Your code passed " + numPassed + " out of " + numTests + " tests.\n\n";
			for(int i=1;i<numFailed+1;i++) {
				//String newLine = System.getProperty("line.separator");
				String[] lines = result.split(i + "\\) ")[1].split("\n");
				String testName = lines[0];
				String reason = lines[1].split("\n")[0];
				LOGGER.finest("Failed test name: " + testName);
				LOGGER.finest("Reason for failure: " + reason);
				processedResults = processedResults + "Unit Test Failed: " + testName + "\n" + "Reason: " + reason + "\n\n";
			}
		}
		else {
			processedResults = "Your code passed all (" + testsRun + ") of the unit tests!";
		}
		return processedResults;
	}

	/**
	 * Deletes the student's directory in the testing directory.
	 */
	public void cleanUp() {
		try {
			FileUtils.deleteDirectory((new File(this.testingDir)).getParentFile());
		} catch (IOException e) {
			LOGGER.warning(e.toString());
		}
	}
}
